﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using Wp7Tools.Services;
using Wp7Tools.Services.Cache;
using Wp7Tools.Util;

namespace Wp7Tools.Controls {
	public static class AsyncImage {

		private static int _maxConcurrentDownloads = 6;
		private static Semaphore _semaphore;
		private static Random _random = new Random(DateTime.Now.Millisecond);

		private static ILocalCache _cache;
		public static ILocalCache Cache {
			get {
				if(_cache == null) {
					if(DesignerProperties.IsInDesignTool) {
						_cache = new FakeLocalCache();
					}
					else {
						_cache = new LocalCache(new DataStorage(), "ImageCache");
					}
				}
				return _cache;
			}
			set { _cache = value; }
		}

		private static Semaphore Semaphore {
			get {
				if (_semaphore == null) {
					_semaphore = new Semaphore(_maxConcurrentDownloads);
				}
				return _semaphore;
			}
		}

		public static void SetMaxConcurrentDownloads(int max) {
			if (_semaphore != null) {
				throw new InvalidOperationException("You can only set max concurrent downloads before instantiating any AsyncImage. Try it in App.xaml");
			}
			_maxConcurrentDownloads = max;
		}

		public static int DelayToDispatcher { get; set; }

		public static Uri GetUriSource(Image image) {
			return (Uri) image.GetValue(UriSourceProperty);
		}

		public static void SetUriSource(Image image, Uri value) {
			image.SetValue(UriSourceProperty, value);
		}

		public static readonly DependencyProperty UriSourceProperty = DependencyProperty.RegisterAttached(
			"UriSourceProperty", typeof (Uri), typeof (AsyncImage), new PropertyMetadata(OnUriSourceChanged));

		private static void OnUriSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
			Uri uri = (Uri) e.NewValue;
			Image image = (Image) d;

			if (DesignerProperties.IsInDesignTool || !uri.IsAbsoluteUri) {
				image.Source = new BitmapImage(uri);
				return;
			}
			ThreadPool.QueueUserWorkItem(o => LimitGoGetImage(image, uri), null);
		}

		private static void LimitGoGetImage(Image image, Uri uri) {
			Semaphore.Run(() => GoGetImage(image, uri));
		}

		private static void GoGetImage(Image image, Uri uri) {
			Debug.WriteLine("AsyncImage -> Getting image: " + uri);
			byte[] imageInBytes = Cache.GetFromCache(UriHelper.ToFilename(uri));
			if (imageInBytes != null) {
				Debug.WriteLine("AsyncImage -> Cached Image: " + uri);
				SetImageToVisual(image, uri, imageInBytes);
				return;
			}

			WebClient client = new WebClient();
			client.OpenReadCompleted += (s, args) => {
			    if (args.Error != null) {
			        Debug.WriteLine("AsyncImage -> Error downloading image:" + args.Error);
			        return;
			    }
			    if (args.Cancelled) {
			        return;
			    }
			    try {
			        byte[] imageFromWeb = ReadFully(args.Result);
			        Cache.SaveToCache(UriHelper.ToFilename(uri), imageFromWeb);
					Debug.WriteLine("AsyncImage -> Cache Miss: " + uri);
			        SetImageToVisual(image, uri, imageFromWeb);
			    }
			    catch (Exception ex) {
			        Debug.WriteLine("AsyncImage -> Error converting stream to image:" + ex);
			    }
			};
			client.OpenReadAsync(uri, client);
		}

		private static void SetImageToVisual(Image image, Uri uri, byte[] imageInBytes) {
			MemoryStream memoryStream = new MemoryStream(imageInBytes);

			if (DelayToDispatcher > 0) {
				Thread.Sleep(_random.Next(0, DelayToDispatcher));
			}
			Deployment.Current.Dispatcher.BeginInvoke(() => {
			    try {
			        BitmapImage im = new BitmapImage();
			        im.SetSource(memoryStream);
			        image.Source = im;
			    }
			    catch {
					Debug.WriteLine("AsyncImage -> Error rendering image: " + uri);
			        //TODO: add broken image
			    }
			});
		}

		public static byte[] ReadFully(Stream input) {
			byte[] buffer = new byte[16*1024];
			using (MemoryStream ms = new MemoryStream()) {
				int read;
				while ((read = input.Read(buffer, 0, buffer.Length)) > 0) {
					ms.Write(buffer, 0, read);
				}
				return ms.ToArray();
			}
		}
	}
}